package com.stars.easyms.datasource;

import com.stars.easyms.base.constant.EasyMsCommonConstants;
import com.stars.easyms.base.util.ClassUtil;
import com.stars.easyms.datasource.autoconfigure.EasyMsDatasourceAutoConfiguration;
import com.stars.easyms.datasource.common.EasyMsDataSourceConstant;
import com.stars.easyms.datasource.dynamic.DynamicLoadDataSourceConfig;
import com.stars.easyms.datasource.enums.DatabaseType;
import com.stars.easyms.datasource.exception.IllegalDataSourcePropertiesException;
import com.stars.easyms.datasource.loadbalancer.LoadBalancer;
import com.stars.easyms.datasource.manual.EasyMsDatasourceManualConfiguration;
import com.stars.easyms.datasource.properties.*;
import com.stars.easyms.datasource.support.DataSourceSupport;
import com.stars.easyms.datasource.support.DataSourceSupportFactory;
import com.stars.easyms.base.util.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.stars.easyms.datasource.common.EasyMsDataSourceConstant.*;

/**
 * <p>className: EasyMsDataSourceFactory</p>
 * <p>description: EasyMs数据源工厂类：用于创建数据源</p>
 *
 * @author guoguifang
 * @version 1.1.0
 * @date 2019-02-22 11:11
 */
@Slf4j
public final class EasyMsDataSourceFactory implements ApplicationContextAware, InitializingBean {

    private static final AtomicBoolean NEED_INIT = new AtomicBoolean(true);

    private static final EasyMsMultiDataSource EASY_MS_MULTI_DATA_SOURCE = new EasyMsMultiDataSource();

    private static final String LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME = "localProperties";

    private static ApplicationContext applicationContext;

    private Resource[] locations;

    /**
     * 创建多数据源
     */
    public EasyMsMultiDataSource createDataSource() {
        if (NEED_INIT.compareAndSet(true, false)) {
            loadLocationProperties();
            loadMultiDataSource();
        }
        return EASY_MS_MULTI_DATA_SOURCE;
    }

    /**
     * 加载多数据源
     */
    public void loadMultiDataSource() {
        // 若未执行createDataSource方法直接支持该方法时需要校验是否初始化
        if (NEED_INIT.compareAndSet(true, false)) {
            loadLocationProperties();
        }

        // 若初始化完成则加载多数据源
        EasyMsMasterSlaveDataSource defaultDataSource = createMasterSlaveDataSource(EasyMsCommonConstants.DEFAULT, true);
        // 使用空字符串表示默认数据源，防止重名
        EASY_MS_MULTI_DATA_SOURCE.addDataSources(EasyMsDataSourceConstant.DEFAULT_DATASOURCE_NAME, defaultDataSource);

        // 初始化多数据源属性
        Set<String> activeDataSourceNameSet = EasyMsPropertiesHelper.getEasyMsDataSourceProperties().getMultiDatasourceActive();
        if (activeDataSourceNameSet != null && !activeDataSourceNameSet.isEmpty()) {
            activeDataSourceNameSet.forEach(activeDataSourceName -> {
                if (StringUtils.isNotBlank(activeDataSourceName)) {
                    EASY_MS_MULTI_DATA_SOURCE.addDataSources(activeDataSourceName,
                            createMasterSlaveDataSource(activeDataSourceName, false));
                }
            });
        }

        // 避免重新刷新把NEED_INIT设置成false
        NEED_INIT.set(false);
    }

    /**
     * 设置重新加载
     */
    public static void reset() {
        NEED_INIT.set(true);
    }

    /**
     * 加载本地属性文件
     */
    private void loadLocationProperties() {
        PropertiesPropertySource propertiesPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, loadProperties());
        BasePropertiesProcessor.setPropertiesPropertySource(propertiesPropertySource);
        EasyMsPropertiesHelper.initProperties();
    }

    /**
     * 创建主从结构数据源
     *
     * @param dataSourceName 数据源名称
     * @param isDefault      是否是默认的数据源
     */
    private EasyMsMasterSlaveDataSource createMasterSlaveDataSource(String dataSourceName, boolean isDefault) {
        // 获取数据源属性的前缀及数据源名称
        EasyMsDataSourcePropertiesProcessor propertiesProcessor = new EasyMsDataSourcePropertiesProcessor(dataSourceName, isDefault);

        // 获取主数据源的数据库类型、连接地址、连接用户名和密码
        DatabaseType databaseType;
        String driverClassName;
        String masterUsername = null;
        String masterPassword = null;
        String masterConnect = null;
        String masterConnectParams = null;

        // 获取主数据源的驱动类、连接url、数据库类型，三者至少有其一用来判断数据库类型
        String masterUrl = propertiesProcessor.getMasterPropertiesValue("url");
        if (StringUtils.isNotBlank(masterUrl)) {
            databaseType = getDatabaseType(dataSourceName, "url", masterUrl);
            driverClassName = databaseType.getDriverClassName();
            masterUsername = propertiesProcessor.getMasterPropertiesValue("username");
            masterPassword = propertiesProcessor.getMasterPropertiesValue("password");
            if (StringUtils.isBlank(masterUsername) || StringUtils.isBlank(masterPassword)) {
                throw new IllegalDataSourcePropertiesException("Master datasource '{}' username and password value must be not empty!", dataSourceName);
            }

            // 如果是mysql则获取主数据源连接参数
            if (DatabaseType.MYSQL_5 == databaseType || DatabaseType.MYSQL_8 == databaseType) {
                masterConnectParams = masterUrl.contains(CONNECT_PARAMS_QUESTION_SIGN) ?
                        masterUrl.substring(masterUrl.indexOf(CONNECT_PARAMS_QUESTION_SIGN) + 1) : "";
            }
        } else {
            driverClassName = propertiesProcessor.getMasterPropertiesValue("driverClassName");
            if (StringUtils.isNotBlank(driverClassName)) {
                databaseType = getDatabaseType(dataSourceName, "driverClassName", driverClassName);
            } else {
                String dbType = propertiesProcessor.getMasterPropertiesValue("dbType");
                if (StringUtils.isNotBlank(dbType)) {
                    databaseType = DatabaseType.forDbType(dbType.trim().toLowerCase());
                    if (databaseType == null) {
                        throw new IllegalDataSourcePropertiesException("Master datasource '{}' key 'dbType' value '{}' not be supported!", dataSourceName, dbType);
                    }
                } else {
                    // 如果driverClassName、masterUrl、dbType三个值都为空的时候默认mysql
                    databaseType = ClassUtil.isExist(DatabaseType.MYSQL_8.getDriverClassName()) ?
                            DatabaseType.MYSQL_8 : DatabaseType.MYSQL_5;
                }
                driverClassName = databaseType.getDriverClassName();
            }

            // 如果连接url未找到，则获取连接地址，连接地址不包含连接参数，连接参数需使用connectParams
            masterConnect = propertiesProcessor.getMasterPropertiesValue("connect");
            if (masterConnect == null) {
                throw new IllegalDataSourcePropertiesException("Master datasource '{}' required key 'connect' not found!", dataSourceName);
            }

            // 如果是mysql则获取主数据源连接参数
            if (DatabaseType.MYSQL_8 == databaseType || DatabaseType.MYSQL_5 == databaseType) {
                masterConnectParams = combineConnectParams(masterConnect, propertiesProcessor.getMasterPropertiesValue("connectParams"), databaseType);
                masterConnectParams = masterConnectParams.startsWith(CONNECT_PARAMS_QUESTION_SIGN) ? masterConnectParams.substring(1) : masterConnectParams;
            }
        }

        // 创建主数据源
        EasyMsDataSource baseMasterDataSource = createBaseDataSource(propertiesProcessor, databaseType, driverClassName, true);
        EasyMsDataSourceSet masterEasyMsDataSourceSet;
        if (masterConnect == null) {
            masterEasyMsDataSourceSet = new EasyMsDataSourceSet();
            baseMasterDataSource.setConnectParams(masterUrl, masterUsername, masterPassword);
            masterEasyMsDataSourceSet.addEasyMsDataSource(baseMasterDataSource);
        } else {
            masterEasyMsDataSourceSet = createMultiDataSources(databaseType, dataSourceName, masterConnect,
                    masterConnectParams, baseMasterDataSource, true);
        }

        // 创建从数据源
        EasyMsDataSourceSet slaveEasyMsDataSourceSet = null;
        String slaveUrl = propertiesProcessor.getSlavePropertiesValue("url");
        if (StringUtils.isNotBlank(slaveUrl)) {
            String slaveUsername = propertiesProcessor.getSlavePropertiesValue("username");
            String slavePassword = propertiesProcessor.getSlavePropertiesValue("password");
            if (StringUtils.isBlank(slaveUsername) || StringUtils.isBlank(slavePassword)) {
                throw new IllegalDataSourcePropertiesException("Slave datasource '{}' username and password value must be not empty!", dataSourceName);
            }

            // 根据url、username、password创建从数据源
            slaveEasyMsDataSourceSet = new EasyMsDataSourceSet();
            EasyMsDataSource baseSlaveDataSource = createBaseDataSource(propertiesProcessor, databaseType, driverClassName, false);
            EasyMsDataSource slaveEasyMsDataSource = baseSlaveDataSource.cloneDataSource();
            slaveEasyMsDataSource.setConnectParams(slaveUrl, slaveUsername, slavePassword);
            slaveEasyMsDataSourceSet.addEasyMsDataSource(slaveEasyMsDataSource);
        } else {
            String slaveConnects = propertiesProcessor.getSlavePropertiesValue("connect");
            if (StringUtils.isNotBlank(slaveConnects)) {
                EasyMsDataSource baseSlaveDataSource = createBaseDataSource(propertiesProcessor, databaseType, driverClassName, false);
                String slaveConnectParams = null;
                if (DatabaseType.MYSQL_8 == databaseType || DatabaseType.MYSQL_5 == databaseType) {
                    slaveConnectParams = propertiesProcessor.getSlavePropertiesValue("connectParams");
                    if (slaveConnectParams == null) {
                        slaveConnectParams = masterConnectParams;
                    } else {
                        slaveConnectParams = slaveConnectParams.startsWith(CONNECT_PARAMS_QUESTION_SIGN) ?
                                slaveConnectParams.substring(1) : slaveConnectParams;
                    }
                }
                slaveEasyMsDataSourceSet = createMultiDataSources(databaseType, dataSourceName, slaveConnects,
                        slaveConnectParams, baseSlaveDataSource, false);
            }
        }

        // 封装数据源
        EasyMsMasterSlaveDataSource dataSource = new EasyMsMasterSlaveDataSource(dataSourceName);
        dataSource.setDatabaseType(databaseType);
        dataSource.addMasterDataSourceSet(masterEasyMsDataSourceSet);
        dataSource.addSlaveDataSourceSet(slaveEasyMsDataSourceSet);

        // 获取数据源切换策略
        dataSource.setMasterLoadBalancer(LoadBalancer.forCode(propertiesProcessor.getMasterPropertiesValue("loadBalancer"), LoadBalancer.ACTIVE_STANDBY));
        dataSource.setSlaveLoadBalancer(LoadBalancer.forCode(propertiesProcessor.getSlavePropertiesValue("loadBalancer"), LoadBalancer.BEST_AVAILABLE));
        return dataSource;
    }

    private DatabaseType getDatabaseType(String dataSourceName, String key, String value) {
        DatabaseType databaseType = DatabaseType.forUrlOrDriverClassName(value.trim().toLowerCase());
        if (databaseType == null) {
            throw new IllegalDataSourcePropertiesException("Master datasource '{}' key '{}' value '{}' not be supported!", dataSourceName, key, value);
        }
        return databaseType;
    }

    private EasyMsDataSource createBaseDataSource(EasyMsDataSourcePropertiesProcessor propertiesProcessor,
                                                  DatabaseType databaseType, String driverClassName, boolean isMaster) {
        DataSourceSupport dataSourceSupport = DataSourceSupportFactory.getDataSourceSupport(propertiesProcessor, isMaster);
        return new EasyMsDataSource(dataSourceSupport, dataSourceSupport.createDataSource(propertiesProcessor, databaseType, driverClassName, isMaster));
    }

    private EasyMsDataSourceSet createMultiDataSources(DatabaseType databaseType, String dataSourceName, String connects,
                                                       String baseConnectParams, EasyMsDataSource baseEasyMsDataSource, boolean isMaster) {
        EasyMsDataSourceSet easyMsDataSourceSet = new EasyMsDataSourceSet();
        int startIndex = 0;
        int commaIndex = connects.indexOf(',', startIndex);
        while (commaIndex > -1) {
            easyMsDataSourceSet.addEasyMsDataSource(createEasyMsDataSource(databaseType, dataSourceName,
                    connects.substring(startIndex, commaIndex).trim(), baseConnectParams, baseEasyMsDataSource, isMaster));
            startIndex = commaIndex + 1;
            commaIndex = connects.indexOf(',', startIndex);
        }
        easyMsDataSourceSet.addEasyMsDataSource(createEasyMsDataSource(databaseType, dataSourceName,
                connects.substring(startIndex).trim(), baseConnectParams, baseEasyMsDataSource, isMaster));
        return easyMsDataSourceSet;
    }


    private EasyMsDataSource createEasyMsDataSource(DatabaseType databaseType, String dataSourceName, String connect,
                                                    String baseConnectParams, EasyMsDataSource baseEasyMsDataSource, boolean isMaster) {
        String connectParams = null;
        // 判断是否包含连接参数，如果包含了连接参数则进行分离
        int questionMarkIndex = connect.indexOf('?');
        if (questionMarkIndex > -1) {
            connectParams = connect.substring(questionMarkIndex + 1);
            connect = connect.substring(0, questionMarkIndex);
        }

        // 如果是mysql则需要获取连接参数
        boolean isUseBaseConnectParams = (DatabaseType.MYSQL_8 == databaseType || DatabaseType.MYSQL_5 == databaseType)
                && StringUtils.isBlank(connectParams);
        if (isUseBaseConnectParams) {
            connectParams = baseConnectParams;
        }

        // 获取连接用户名和密码并判断数据源用户名密码是否为空
        int atIndex = connect.indexOf(CONNECT_URL_AT_SIGN);
        int slashIndex;
        if (atIndex == -1 || (slashIndex = connect.indexOf(CONNECT_URL_FORWARD_SLASH_SIGN)) == -1 || slashIndex > atIndex) {
            throw new IllegalDataSourcePropertiesException((isMaster ? MASTER : SLAVE)
                    + " datasource '{}' key 'connect' value is wrong!", dataSourceName);
        }
        String[] usernameAndPassword = connect.substring(0, atIndex).split(CONNECT_URL_FORWARD_SLASH_SIGN);
        if (usernameAndPassword.length != 2 || StringUtils.isBlank(usernameAndPassword[0]) || StringUtils.isBlank(usernameAndPassword[1])) {
            throw new IllegalDataSourcePropertiesException((isMaster ? MASTER : SLAVE)
                    + " datasource '{}' username and password value must be not empty!", dataSourceName);
        }

        // 对原数据源copy生成新数据源
        EasyMsDataSource cloneDataSource = baseEasyMsDataSource.cloneDataSource();
        cloneDataSource.setConnectParams(formatConnectUrl(databaseType, dataSourceName, connect.substring(atIndex + 1),
                connectParams, isMaster), usernameAndPassword[0], usernameAndPassword[1]);
        return cloneDataSource;
    }

    private String formatConnectUrl(DatabaseType databaseType, String dataSourceName, String connectAddress,
                                    String connectParams, boolean isMaster) {
        int colonIndex = connectAddress.indexOf(CONNECT_URL_COLON_SIGN);
        int slashIndex = connectAddress.indexOf(CONNECT_URL_FORWARD_SLASH_SIGN);
        if (colonIndex == -1 || connectAddress.lastIndexOf(CONNECT_URL_COLON_SIGN) != colonIndex || slashIndex == -1
                || connectAddress.lastIndexOf(CONNECT_URL_FORWARD_SLASH_SIGN) != slashIndex || colonIndex > slashIndex) {
            throw new IllegalDataSourcePropertiesException((isMaster ? MASTER : SLAVE)
                    + " datasource '{}' key 'connect' value '{}' does not have the correct format 'ip:port/database'!",
                    dataSourceName, connectAddress);
        }
        return databaseType.getConnectPrefix() + (DatabaseType.MYSQL_8 == databaseType || DatabaseType.MYSQL_5 == databaseType ?
                connectAddress + CONNECT_PARAMS_QUESTION_SIGN + connectParams :
                connectAddress.replace(CONNECT_URL_FORWARD_SLASH_SIGN, databaseType.getConnectSuffix()));
    }

    @NonNull
    private String combineConnectParams(String masterConnect, String connectParams, DatabaseType databaseType) {
        String masterConnectParams = null;
        int masterConnectQuestionSignIndex = masterConnect.indexOf(CONNECT_PARAMS_QUESTION_SIGN);
        if (masterConnectQuestionSignIndex > -1) {
            masterConnectParams = masterConnect.substring(masterConnectQuestionSignIndex);
        }
        if (masterConnectParams == null && connectParams == null) {
            return databaseType.getDefaultConnectParams();
        }
        Map<String, String> connectParamsMap = new LinkedHashMap<>(databaseType.getDefaultConnectParamsMap());
        combineConnectParamMap(masterConnectParams, connectParamsMap);
        combineConnectParamMap(connectParams, connectParamsMap);
        StringBuilder sb = new StringBuilder();
        connectParamsMap.forEach((k, v) -> sb.append(CONNECT_PARAMS_JOINT_SIGN)
                .append(k).append(CONNECT_PARAMS_EQUAL_SIGN).append(v));
        return sb.substring(1);
    }

    private void combineConnectParamMap(@Nullable String connectParams, Map<String, String> connectParamsMap) {
        if (connectParams == null) {
            return;
        }
        if (connectParams.startsWith(CONNECT_PARAMS_QUESTION_SIGN)) {
            connectParams = connectParams.substring(1);
        }
        for (String connectParamsItem : connectParams.split(CONNECT_PARAMS_JOINT_SIGN)) {
            String[] entry = connectParamsItem.split(CONNECT_PARAMS_EQUAL_SIGN);
            if (entry.length == 2) {
                connectParamsMap.put(entry[0], entry[1]);
            }
        }
    }

    public EasyMsDataSourceFactory() {
    }

    public EasyMsDataSourceFactory(Resource[] locations) {
        this.locations = locations;
    }

    public void setLocations(Resource[] locations) {
        this.locations = locations;
    }

    private Properties loadProperties() {
        Properties props = new Properties();
        if (this.locations != null) {
            for (Resource location : this.locations) {
                try {
                    PropertiesLoaderUtils.fillProperties(props, new EncodedResource(location, StandardCharsets.UTF_8));
                } catch (IOException e) {
                    throw new IllegalDataSourcePropertiesException("Could not load datasource properties[{}]!", location, e);
                }
            }
        }
        return props;
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
        setStaticApplicationContext(applicationContext, this);
    }

    private static void setStaticApplicationContext(ApplicationContext applicationContext, EasyMsDataSourceFactory easyMsDataSourceFactory) {
        if (EasyMsDataSourceFactory.applicationContext == null) {
            EasyMsDataSourceFactory.applicationContext = applicationContext;
            ApplicationContextHolder.setApplicationContext(applicationContext);
            // 开启动态加载数据源
            new DynamicLoadDataSourceConfig(easyMsDataSourceFactory).start(applicationContext);
        }
    }

    public static EasyMsMultiDataSource getEasyMsMultiDataSource() {
        return EASY_MS_MULTI_DATA_SOURCE;
    }

    /**
     * 用于spingmvc项目的EasyMsDatasourceAutoConfiguration类注册
     */
    @Override
    public void afterPropertiesSet() {
        /*
         * 判断是否是springmvc，如果是springmvc则不会自动加载EasyMsDatasourceAutoConfiguration类，
         * 这里不能使用ConfigurationClassPostProcessor注册，否则会造成EasyMsDatasourceAutoConfiguration类中部分bean无法创建，所以需要手动创建每一个bean
         */
        if (EasyMsDataSourceFactory.applicationContext != null) {
            try {
                EasyMsDataSourceFactory.applicationContext.getBean(EasyMsDatasourceAutoConfiguration.class);
            } catch (BeansException e) {
                EasyMsDatasourceManualConfiguration easyMsDatasourceManualConfiguration = new EasyMsDatasourceManualConfiguration();
                easyMsDatasourceManualConfiguration.manualRegisterBean((ConfigurableApplicationContext) EasyMsDataSourceFactory.applicationContext);
            }
        }
    }
}